gbvs Mini-Challenge 2
Bearbeitet durch Si Ben Tran im HS 2023.
Bachelor of Science FHNW in Data Science.
In dieser Mini-Challenge werden LE3 und LE4 von gbsv geprüft. In Data Science und Machine Learning werden oft Merkmale (Features) von Bildern und Signalen generiert, um basierend darauf zu analysieren oder zu lernen. Diesem Thema widmen wir uns mittels klassischer Signal- und Bildverarbeitung.
Jede:r Studierende:r hat eine individualisierte Aufgabenstellung. Die Abgabe soll ebenfalls einzigartig sein.
Die Programmiersprache und die Code-Dokumentation darf frei gewählt werden. Sofern nicht anders erwähnt, dürfen vorhandene Bibliotheken verwendet werden. Gebe die Quellen deiner Daten und ggf. deines Codes an.
Checkpoints:
Aufgabenstellung Rebuttal: In dieser Phase kann die Aufgabenstellung gereviewt und überarbeitet werden. Sofern die Studierenden entsprechende Argumente haben. Bis ca. 1 Woche nach MC-Start am Tag der Sprechstunde. 5. Dez. 2023
Review ausgewählte Daten: Reviewe deine ausgewählten Daten bis 3 Wochen nach MC-Start mit der Fachexpertin. Das Review kann schriftlich oder in der Sprechstunde erfolgen. 12. Dez. 2023
Anonymisierte Abgabe im Peer-Grading-Tool: Code, Resultate und Report gemäss Vorlage. Es können aktuell max. 100MB aufs Peer-Grading Tool geladen werden. 12. Jan. 2024
Peer-Grading von anderen Abgaben. In letzten Sprechstunde im Semester oder bis 19. Jan 2024.
Die Termine sind jeweils um 23:59.
Setup und Imports¶
%load_ext autoreload
%autoreload 2
import os
os.chdir('../')
import cv2 as cv
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from PIL import Image
from statsmodels.tsa.stattools import acf
from skimage import img_as_float
from skimage.segmentation import chan_vese
# Signal
from src.signal_processor import slice_signal, plot_data_and_correlation
from src.signal_processor import SignalProcessor
# Image
from src.image_processor import plot_2_images, plot_three_images, display_images
from src.image_processor import apply_color_threshold, threshold_filter_all_colors, convert_to_grayscale
from src.image_processor import convert_to_grayscale, extract_object, skeleton, n_pixel_in_mask
# Feature Descriptor
from src.feature_descriptor import rotate_image, create_SIFT, match_images_with_brisk
1 Mustersuche in Bild und Signal (LE3)¶
1.1. Korrelation in Signalen¶
Suche 1 Signal, welches wiederkehrende Muster enthält. Analyisere dann mittels Auto-Korrelation die wiederkehrenden Muster innerhalb deines Signals. Kann die Periodizität deines Musters via Auto-Korrelogramm sichtbar gemacht werden? Diskutiere deine Methoden- und Parameterwahl sowie die Resultate in ca. 150 Wörtern. Schneide nun ein Stück deines Signals aus und versuche es via Kreuzkorrelation im Ursprungssignal zu finden. Woran erkennst du, dass die Stelle passt? Beschreibe in 1-2 Sätzen. Verändere nun dein ausgeschnittenes Stück etwas und schaue, ob es immer noch via Kreuzkorrelation gefunden werden kann. Welche Arten von Veränderungen werden toleriert? Welche nicht? Diskutiere die Resultate in ca. 150 Wörtern.
Achtung: dies ist eine offene Aufgabenstellung. Setzt euch selbst einen Rahmen für die Beschränkung oder kommt in die Sprechstunde, sofern euch die Grenzen nicht klar sind. Treffende Datenwahl, auf den Punkt gebrachte Kreativität, massvolle Vielfalt und kritisches Denken sind gesucht.
1.1.1 Signal mit wiederkehrende Muster¶
Der Code generiert ein synthetisches Signal, indem er eine Kombination von Sinus- und Cosinusfunktionen mit verschiedenen Potenzen verwendet und Rauschen hinzufügt. Die Parameter des Signals, wie die Zeitdauer, Frequenz, Amplitude und Rauschamplitude, werden zuvor festgelegt. Das generierte Signal wird in einem DataFrame gespeichert und anschliessend in einem Diagramm visualisiert, das die Zeit auf der x-Achse und das Signal auf der y-Achse darstellt. In der Visualisierung ist ein wiederkehrendes Muster zu erkennen.
np.random.seed(42)
time = np.linspace(0, 10, 1000)
frequency = 1
amplitude = 1
noise_amplitude = 0.25
signals = amplitude * np.sin(1 * np.pi * frequency * time) ** 3 + \
amplitude * np.cos(3 * np.pi * frequency * time) ** 5 + \
amplitude * np.cos(4 * np.pi * frequency * time) ** 7 + \
noise_amplitude * np.random.normal(size=len(time), scale=0.5)
data = pd.DataFrame({'time': time, 'signal': signals})
display(data)
fig = plt.figure(figsize=(12, 6))
plt.plot(time, signals)
plt.xlabel('Zeit in (s)')
plt.ylabel('Signal')
plt.title('Signal vs Zeit')
plt.show()
| time | signal | |
|---|---|---|
| 0 | 0.00000 | 2.062089 |
| 1 | 0.01001 | 1.906697 |
| 2 | 0.02002 | 1.794959 |
| 3 | 0.03003 | 1.607561 |
| 4 | 0.04004 | 1.063090 |
| ... | ... | ... |
| 995 | 9.95996 | 1.053273 |
| 996 | 9.96997 | 1.640221 |
| 997 | 9.97998 | 1.793607 |
| 998 | 9.98999 | 1.852520 |
| 999 | 10.00000 | 2.071573 |
1000 rows × 2 columns
1.1.2 Auto-Korrelation des Signals¶
Dokumentation für die Kreuzkorrelation von Statsmodel: Link
Dieser Code erstellt Autokorrelationsdiagramme für ein gegebenes Signal bei verschiedenen zeitlichen Verzögerungen. Es zeigt sowohl das Originalsignal als auch die Autokorrelogramme für ausgewählte Verzögerungen. Die Autokorrelogramme zeigen, wie stark das Signal mit sich selbst bei verschiedenen Verzögerungen korreliert ist. Dies ist hilfreich, um periodische Muster oder Abhängigkeiten im Signal zu analysieren.
In dem Autokorrelogramm erkennen wir, dass das Signal durch steigende Anzahl an Lags immer weniger korreliert. Dies hat jedoch mit der Library zu tun, da entweder der Nenner vom der Gesamtenlänge des Signales in betrachtet gezogen wird, oder nur der Abschnitt bis zu dem Lag. Nach ca. einem Lag von 21 fallen wir mit der Korrelation unter 0 und werden sogar negativ. Erst ab einem Lag von ca. 42 steigen wir wieder über 0 und werden positiv. Bei einer höheren Anzahl an Lags bis zu 250 erkennen wir ein kleines Muster zwischen Lag ca. 40 bis 100 und danach die Spieglung des Muster von 100 bis ca. 190. Das Muster lässt sich als ein lang gezogenes schnürlischrift Buchstabe "W" beschreiben. Erhöhen wir den Lag bis auf die maximale Anzahl an Zeilen die in unserem Signaldaten vorhanden ist, so sehen wir ein Muster, dass sich wiederholt, das zu unserem Ursprünglichen Signal einer Kombination aus Sinus- und Cosinusfunktionen passt.
lags_to_plot = [0, 10, 50, 100, 250, len(signals)]
fig, axes = plt.subplots(len(lags_to_plot), 1, figsize=(10, 12))
for i, lag in enumerate(lags_to_plot):
autocorrelation = acf(signals, nlags=lag)
if i == 0:
axes[i].plot(time, signals)
axes[i].set_title('Original Signal')
axes[i].set_xlabel('Time')
axes[i].set_ylabel('Amplitude')
else:
lags = np.arange(len(autocorrelation))
axes[i].stem(lags, autocorrelation, basefmt = "C2-")
axes[i].set_title(f'Autocorrelogram (Lag = {lag})')
axes[i].set_xlabel('Lag')
axes[i].set_ylabel('Autocorrelation')
plt.tight_layout()
plt.show()
1.1.3 Auschnitt aus dem Signal¶
Die Funktion slice_signal dient dazu, ein DataFrame zu zerschneiden und einen zufälligen Abschnitt mit einer bestimmten Länge daraus auszuwählen. Das Ergebnis, der ausgewählte Abschnitt, wird in einem neuen DataFrame namens sliced_signal gespeichert und zurückgegeben. Dieser Code ermöglicht es, zufällige Teile von Daten für die Kreuzkorrelation zu verwenden.
slice_length = 100
sliced_data = slice_signal(data, slice_length)
display(sliced_data)
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
axes[0].plot(data['time'], data['signal'], label='Original Data', color='blue')
axes[0].set_xlabel('Time (s)')
axes[0].set_ylabel('Signal')
axes[0].set_title('Original Data')
axes[1].plot(sliced_data['time'], sliced_data['signal'], label='Sliced Data', color='green')
axes[1].set_xlabel('Time (s)')
axes[1].set_ylabel('Signal')
axes[1].set_title('Sliced Data')
axes[2].plot(data['time'], data['signal'], label='Original Data', color='blue', linestyle='--', alpha=0.75)
axes[2].plot(sliced_data['time'], sliced_data['signal'], label='Sliced Data', color='green')
axes[2].set_xlabel('Time (s)')
axes[2].set_ylabel('Signal')
axes[2].set_title('Original and Sliced Data')
axes[0].legend()
axes[1].legend()
axes[2].legend()
plt.tight_layout()
plt.show()
| time | signal | |
|---|---|---|
| 102 | 1.021021 | -0.168027 |
| 103 | 1.031031 | -0.327068 |
| 104 | 1.041041 | -0.326542 |
| 105 | 1.051051 | -0.289076 |
| 106 | 1.061061 | -0.086617 |
| ... | ... | ... |
| 197 | 1.971972 | 1.498527 |
| 198 | 1.981982 | 1.771660 |
| 199 | 1.991992 | 1.808079 |
| 200 | 2.002002 | 2.041621 |
| 201 | 2.012012 | 1.961619 |
100 rows × 2 columns
1.1.4 Kreuzkorrelation des Auschnitts mit Orginalsignal¶
Dokumentation für die Kreuzkorrelation von Statsmodel: Link
Dieser Code erstellt Kreuzkorrelationsdiagramme für ein gegebenes Signal bei verschiedenen zeitlichen Verzögerungen. Es zeigt sowohl das Originalsignal als auch die Kreuzkorrelation für ausgewählte Verzögerungen. Die Kreuzkorrelation zeigt, wie stark das Signal mit einem anderen Signal bei verschiedenen Verzögerungen korreliert ist. Dies ist hilfreich, um periodische Muster oder Abhängigkeiten im Signal zu analysieren. In unserem Fall erkennen wir quantiativ, dass unser ausgeschnittenes Signal mit dem Orginal Signal 5 x identisch überlappen lässt. Dies ist auch in der Kreuzkorrelationsdiagramm zu erkennen, dass 5 Peaks aufweisst mit einem hohen Korrelationswert vong grösser als 0.8.
plot_data_and_correlation(data, sliced_data, single_row=True, data_name='Sliced Data')
1.1.5 Auschnitt verändern¶
Wir definieren uns eine Klasse namens SignalProcessor die uns erlaubt, die Signale zu verändern. Die Klasse hat folgende Methoden:
| Methode Name | Beschreibung |
|---|---|
addition_noise |
Fügt dem Signal eine konstante hinzu. |
multiplication_noise |
Multipliziert das Signal mit einer konstante. |
addition_noise_random |
Fügt dem Signal eine zufällige konstante hinzu. |
multiplication_noise_random |
Multipliziert das Signal mit einer zufälligen Konstanten. |
standarize_signal |
Standarisiert das Signal. |
normalize_min_max_signal |
Normalisiert das Signal (min-max). |
shuffle_signal |
Mischelt das Signal. |
reverse_signal |
Dreht das Signal um. |
log_transform_signal |
Logarithmiert das Signal. |
Es gibt durchaus weitere Transformationen die man mit den Signalen hätte machen können, wie bsp die Wurzel ziehen oder das Signal zu potentieren, oder die Signal in unterschiedlichen Kombinationen zu verändern. Jedoch haben wir uns für die oben genannten Transformationen entschieden, um den Rahmen der Challenge nicht zu sprengen.
print([method for method in dir(SignalProcessor) if not method.startswith('_')])
['add_random_noise', 'addition_noise', 'log_transform_signal', 'multiply_noise', 'multiply_random_noise', 'normalize_min_max_signal', 'reverse_signal', 'shuffle_signal', 'standardize_signal']
1.1.6 Kreuzkorrelation des veränderten Auschnitts mit Orginalsignal¶
Aus dem vorherhigen Kapitel 1.1.5 nutzen wir alle Methoden um unser geschnittenes Signal zu transformieren und berechnen anschliessend dann die Kreuzkorrelation und visualisieren diese. Die Beobachtungen halten wir hier als Tabelle überischtlich fest:
| Methode Name | Beschreibung | Beobachtung |
|---|---|---|
addition_noise |
Fügt dem Signal eine konstante hinzu. | Addieren wir einen Konstante zu unserem Auschnitt, so erkennen wir, dass die Kreuzkorrelations Diagramm zeigt, dass die Korrelationen zum Orginalen Signalen nach wie vor gefunden werden kann. Vergleichen wir diese mit unserer Kreuzkorrelationdiagramm indem der Signalauschnitt nicht verändert wurde, so sehen wir keinen grossen Unterschied zwischen den beiden Liniendiagrammen. |
multiplication_noise |
Multipliziert das Signal mit einer konstante. | Auch durch die Multiplikation des ausgeschnittenen Signales, ist es nach wie vor möglich den Auschnitt im Orginalen Signal zu finden. Die Kreuzkorrelationsdiagramme sehen auch hier identisch aus wie zum Orginalen Kreuzkorrelationsplot. |
addition_noise_random |
Fügt dem Signal eine zufällige konstante hinzu. | Durch das hinzufügen einer zufälligen Konstante für jeden Signalwert, ist es uns durchaus noch gelungen das Kreuzkorrelationsmuster von der nähe von 1 zu erreichen. Man könnte somit meinen, dass die Addition durch einen Random Wert keinen Einfluss hat und der Auschnitt nach wie vor gefunden werden kann im Orginalen Signal Daten. |
multiplication_noise_random |
Multipliziert das Signal mit einer zufälligen Konstanten. | Multiplizieren wir die Signale mit unterschiedlichen Werten, so erkennen wir nun, dass wir das Orginale Muster nicht mehr in unserm Orignalen Signaldaten wiederfinden können, bzw. die Korrelation dazu schlecht wurde. Auch in der Visualisierung sehen wir, dass die Multiplikation des Auschnittes das Auschnittsignal um ein vielfaches grösser werden lässt als das Orginale. |
standarize_signal |
Standarisiert das Signal. | Durch das Standardisieren des Signales, indem wir von jedem Element den Mittelwert subtrahieren und dividieren durch die Standardabweichung, erkennen wir, dass das Signal immer noch im Orginalen gefunden werden kann. Somit hat die Standardisieren keine Auswirkung auf die Kreuzkorrelation. |
| normalize_min_max_signal | Normalisiert das Signal (min-max). | Interessant bei der Min-Max Normalisierung ist, dass wir auch hier wie beim standarize_signal den Auschnitt im Orginalen wiederfinden können. Betrachten wir jedoch nun das Signal verglichen mit dem Orginalen Signal, so könnte man im ersten Moment meinen, dass ein hoher Korrelationswert nicht möglich sei.
| shuffle_signal | Mischelt das Signal. | Beim shuffeln des Signales ist es wie zu erwarten, dass wir das Orginale Muster nicht mehr wiederfinden können. |
| reverse_signal | Dreht das Signal um. | Durch das reversen (bzw. Drehen des Ausschnitt Signales) sehen wir dass wir nur eine leichte Kreuzkorrelationswerte zu unserem Orginalen Muster finden können. Interessanterweise, können wir nun verstärkt eine negativ Kreuzkorrelation finden verglichen zu unserem Orginalen Kreuzkorrelations Liniendiagramm. |
| log_transform_signal | Logarithmiert das Signal. | Beim Logarithmieren des Auschnitt Signales, verringern wir die Kreuzkorrelationswerte. Das Auschnitts Signal ist nun nicht mehr wieder zu finden im Orginalen Signal bzw. nur noch sehr schwach. |
Fassen wir nun zusammen, welche Transformationen wir durchgeführt haben auf den Ausgeschnittene Signal und diese wieder im Orginalen Signal wiederfinden konnten.
| Methode Name | Wiederfinden im Orginalen Signal |
|---|---|
addition_noise |
Ja |
multiplication_noise |
Ja |
addition_noise_random |
Ja |
multiplication_noise_random |
Nein |
standarize_signal |
Ja |
normalize_min_max_signal |
Ja |
shuffle_signal |
Nein |
reverse_signal |
Nein |
log_transform_signal |
Nein |
signal_processor = SignalProcessor(sliced_data)
sliced_data_addition = signal_processor.addition_noise(noise=5)
sliced_data_multiply = signal_processor.multiply_noise(noise=2)
sliced_data_add_random = signal_processor.add_random_noise(noise_amplitude=0.5)
sliced_data_multiply_random = signal_processor.multiply_random_noise(noise_amplitude=7)
sliced_data_standardize = signal_processor.standardize_signal()
sliced_data_normalize_min_max = signal_processor.normalize_min_max_signal()
sliced_data_shuffle = signal_processor.shuffle_signal()
sliced_data_reverse = signal_processor.reverse_signal()
sliced_data_log_transform = signal_processor.log_transform_signal()
sliced_data_processed_dict = {
"sliced_data": sliced_data,
"sliced_data_addition": sliced_data_addition,
"sliced_data_multiply": sliced_data_multiply,
"sliced_data_add_random": sliced_data_add_random,
"sliced_data_multiply_random": sliced_data_multiply_random,
"sliced_data_standardize": sliced_data_standardize,
"sliced_data_normalize_min_max": sliced_data_normalize_min_max,
"sliced_data_shuffle": sliced_data_shuffle,
"sliced_data_reverse": sliced_data_reverse,
"sliced_data_log_transform": sliced_data_log_transform
}
for name, processed_data in sliced_data_processed_dict.items():
plot_data_and_correlation(data, processed_data, single_row=True, data_name=name)
1.2 Segmentierung, morphologische Operationen, Objekteigenschaften in Bildern¶
Suche 1 Bild, welches mehrere ähnliche Objekte enthält. Diese Objekte sollen mittels geeigneter Methoden segmentiert werden. Die Resultate sollen als gelabelte Bilder (binär oder pro Klasse 1 Label) gespeichert werden. Zeige dabei, wie du die Labelmasken mittels morphologischer Operationen verbessert hast. Erkläre hier für jede angewendete Operation in 1-2 Sätzen, warum du diese Operation anwendest. Zeige auch in Einzelbildern die Zwischenresultate deiner angewendeten Operationen und dass du nur minimal die Objekte verändert hast (z.B. keine Verschiebungen, Verkleinerungen oder Vergrösserungen). Extrahiere am Ende deine einzelnen Objekte, zähle und vermesse 2-3 Eigenschaften deiner extrahierten Objekte mittels geeigneten Methoden. Erkläre pro Eigenschaft in 1-2 Sätzen, warum du diese gewählt hast und ob die Resultate brauchbar sind.
Erstelle dann ein möglichst minimales aber repräsentatives Skeleton eines deiner Objekte und gebe die Anzahl Pixel des Skeletons aus.
Diskutiere deine Erkenntnisse und Resultate in ca. 150 Wörtern.
Weiterführende Links: skimage: Label image regions skimage: Segment human cells (in mitosis) skimage: Measure region properties
1.2.1 Bild mit mehreren ähnlichen Objekten¶
Als Bild habe ich hier mein Lieblings Pokemon gewählt, das Enton.
Die Eigenschaften eines Entons sin:
- Gelber Körper
- Beiger Schnabel und Füsse
- Weisse Augen mit schwarzen Pupillen und Haaren
- Enton ist eine Ente
Hier habe ich zwei Bilder von Entons ausgewählt. In einem Bild sehen wir Enton in seiner freien wilden Natur und im anderen Bild sind die Entons als Handy Hintergrundbild zu sehen.
image1 = Image.open('data/image/wilde-entons.jpg')
image2 = Image.open('data/image/handy-entons.jpg')
titles = ['Wilde Entons\nBild Dimension: {} x {}'.format(image1.height, image1.width), 'Entons Handyhintergrund\nBild Dimension: {} x {}'.format(image2.height, image2.width)]
plot_2_images(image1, image2, titles)
image1_array = np.array(image1)
image2_array = np.array(image2)
image1_gray = image1.convert("L")
image2_gray = image2.convert("L")
1.2.2 Segmentierung mittels chan-vese¶
Die Segmentierung mittels chan-vese funktionierte nicht wie erwartet. Für das einfache Enton Handy Hintergrundbild konnte die Segmentierung gut gemacht werden, je doch für das komplexe wilde Enton Bild, müsste man hier noch mehrere Anpassungen machen. Die Segmentierung hier habe ich nicht mehr weiter optimiert und habe dann mittels manueller Thresholding Methode versucht die Entons besser zu segmentieren. Eine Problematik für den Chan-Vese Algorithmus beim wilden Enton Bild könnte sein, dass diese nur auf schwarz-weiss Bilder gut funktioniert in denen die Farbvielfalt nicht so gross ist, was beim Wilden Enton Bild jedoch nicht der Fall ist.
image1_array_float = img_as_float(image1_gray)
image2_array_float = img_as_float(image2_gray)
cv1 = chan_vese(image1_array_float, mu=0.25, lambda1=1, lambda2=1, tol=1e-3,
max_num_iter=200, dt=0.5, init_level_set="checkerboard",
extended_output=True)
cv2 = chan_vese(image2_array_float, mu=0.25, lambda1=1, lambda2=1, tol=1e-3,
max_num_iter=200, dt=0.5, init_level_set="checkerboard",
extended_output=True)
fig, axes = plt.subplots(2, 2, figsize=(8, 8))
ax = axes.flatten()
ax[0].imshow(image1_array_float, cmap="gray")
ax[0].set_axis_off()
ax[0].set_title("Original Image 1", fontsize=12)
ax[1].imshow(cv1[0], cmap="gray")
ax[1].set_axis_off()
title = "Chan-Vese segmentation - {} iterations".format(len(cv1[2]))
ax[1].set_title(title, fontsize=12)
ax[2].imshow(image2_array_float, cmap="gray")
ax[2].set_axis_off()
ax[2].set_title("Original Image 2", fontsize=12)
ax[3].imshow(cv2[0], cmap="gray")
ax[3].set_axis_off()
title = "Chan-Vese segmentation - {} iterations".format(len(cv2[2]))
ax[3].set_title(title, fontsize=12)
fig.tight_layout()
plt.show()
1.2.2 Segmentierung mittels Thresholding¶
Wir Segmentieren unsere Entons in dem wir hier eine Funktion definieren und unterschiedliche Thresholds für jeden Farbchannel anwenden. So können wir quantitativ bestimmen, ob die Segmentierung mittels Thresholding gut funktioniert hat. Wichtig hier ist, dass sobald der Pixelwert unter dem Treshold liegt, wird der Pixel auf 0 gesetzt.
Im zweiten Codeblock dieses Kapitels erweitern wir die Funktion damit wir viele mögliche Thresholds ausprobieren können und exportieren dann jeweils die Bilder. Somit können wir dann quantiativ bestimmen, welcher Threshold für die Bilder am besten geeignet ist. Die Exportierten Bilder sind unter dem Ordner data/export zu finden.
Wie zu erwarten ist das Segmentieren von Handy Entons deutlich einfacher als die von wilden Entons. Das liegt daran, dass wir eine einheitliche Hintergrundfarbe haben die wir entfernen müssen. Bei den wilen Entons stört uns der Hintergrund bzw. Beige Felsen die eine ähnliche Farbe aufweist wie die vom Schnabel und Füsse. Dies ist bei der Segmentierung nicht optimal, da wir somit die Schnabel und Füsse nicht mehr erkennen können bzw. der Hintergrund dann im späteren Kapitel mühsam durch Dilation und Erosion entfernt werden müssen.
Im Letzten Code Abschnitt visualisieren wir vier Bilder nebeneinandern. Auch hier erkennen wir bereits, dass die Segmentierung der Handy Entons deutlich besser funktioniert hat als die von den wilden Entons aufgrund von der Segmentierungs Maske.
filter_image1 = apply_color_threshold(image1_array, 200, 180, 0)
filter_image2 = apply_color_threshold(image2_array, 72, 0, 0)
titles_filter = ['Wilde Entons - Segmentation', 'Entons Handyhintergrund - Segmentation']
plot_2_images(filter_image1, filter_image2, titles_filter)
threshold_filter_all_colors(export_path='data/export/wilde-entons/',
import_path='data/image/wilde-entons.jpg',
range_r=(195, 205), range_g=(175, 185), range_b=(0, 1))
threshold_filter_all_colors(export_path='data/export/handy-entons/',
import_path='data/image/handy-entons.jpg',
range_r=(60, 80), range_g=(0, 1), range_b=(0, 1))
Files already exist in the folder. Files already exist in the folder.
threshold_r_handy = 72
threshold_g_handy = 0
threshold_b_handy = 0
threshold_r_wilde = 200
threshold_g_wilde = 180
threshold_b_wilde = 0
image_handy = Image.open('data/image/handy-entons.jpg')
image_handy_array = np.array(image_handy)
image_wilde = Image.open('data/image/wilde-entons.jpg')
image_wilde_array = np.array(image_wilde)
image_segmentation = Image.open('data/export/handy-entons/pokemon-image-thresh-r-72_g-0_b-0.jpg')
image_seg_array_handy = np.array(image_segmentation)
image_segmentation_wilde = Image.open('data/export/wilde-entons/pokemon-image-thresh-r-200_g-180_b-0.jpg')
image_seg_array_wilde = np.array(image_segmentation_wilde)
grey_image = image_handy.convert('L')
grey_image_array = np.array(grey_image)
grey_image_wilde = image_wilde.convert('L')
grey_image_array_wilde = np.array(grey_image_wilde)
image_mask = image_seg_array_handy.copy()
image_mask[image_mask[:, :, 0] > 0] = 255
image_mask[image_mask[:, :, 1] > 0] = 255
image_mask[image_mask[:, :, 2] > 0] = 255
image_mask_wilde = image_seg_array_wilde.copy()
image_mask_wilde[image_mask_wilde[:, :, 0] > 0] = 255
image_mask_wilde[image_mask_wilde[:, :, 1] > 0] = 255
image_mask_wilde[image_mask_wilde[:, :, 2] > 0] = 255
print(f'Number of 0 pixels in channel r: {np.sum(image_seg_array_handy[:, :, 0] == 0)}')
print(f'Number of 0 pixels in channel g: {np.sum(image_seg_array_handy[:, :, 1] == 0)}')
print(f'Number of 0 pixels in channel b: {np.sum(image_seg_array_handy[:, :, 2] == 0)}')
print(f'Total number of pixels: {image_seg_array_handy.size}')
print(f'Relation of 0 pixels in channel r: {np.sum(image_seg_array_handy[:, :, 0] == 0) / image_seg_array_handy[:, :, 0].size}')
print(f'Relation of 0 pixels in channel g: {np.sum(image_seg_array_handy[:, :, 1] == 0) / image_seg_array_handy[:, :, 1].size}')
print(f'Relation of 0 pixels in channel b: {np.sum(image_seg_array_handy[:, :, 2] == 0) / image_seg_array_handy[:, :, 2].size}')
print(f'Number of 0 pixels in channel r: {np.sum(image_seg_array_wilde[:, :, 0] == 0)}')
print(f'Number of 0 pixels in channel g: {np.sum(image_seg_array_wilde[:, :, 1] == 0)}')
print(f'Number of 0 pixels in channel b: {np.sum(image_seg_array_wilde[:, :, 2] == 0)}')
print(f'Total number of pixels: {image_seg_array_wilde.size}')
print(f'Relation of 0 pixels in channel r: {np.sum(image_seg_array_wilde[:, :, 0] == 0) / image_seg_array_wilde[:, :, 0].size}')
print(f'Relation of 0 pixels in channel g: {np.sum(image_seg_array_wilde[:, :, 1] == 0) / image_seg_array_wilde[:, :, 1].size}')
print(f'Relation of 0 pixels in channel b: {np.sum(image_seg_array_wilde[:, :, 2] == 0) / image_seg_array_wilde[:, :, 2].size}')
Number of 0 pixels in channel r: 2110115 Number of 0 pixels in channel g: 2084416 Number of 0 pixels in channel b: 2099146 Total number of pixels: 8294400 Relation of 0 pixels in channel r: 0.763207103587963 Relation of 0 pixels in channel g: 0.753912037037037 Relation of 0 pixels in channel b: 0.7592397280092592 Number of 0 pixels in channel r: 1640230 Number of 0 pixels in channel g: 1638938 Number of 0 pixels in channel b: 1661356 Total number of pixels: 6220800 Relation of 0 pixels in channel r: 0.7910059799382716 Relation of 0 pixels in channel g: 0.7903829089506172 Relation of 0 pixels in channel b: 0.8011940586419753
grey_image_handy = convert_to_grayscale(image_handy)
display_images(image_handy, image_segmentation, grey_image_handy, image_mask, f'Segmentation Image \nBild Dimension: {image_segmentation.height} x {image_segmentation.width} \nthreshold_r: {threshold_r_handy}, threshold_g: {threshold_g_handy}, threshold_b: {threshold_b_handy}')
grey_image_wilde = convert_to_grayscale(image_wilde)
display_images(image_wilde, image_segmentation_wilde, grey_image_wilde, image_mask_wilde, f'Segmentation Image \nBild Dimension: {image_segmentation_wilde.height} x {image_segmentation_wilde.width} \nthreshold_r: {threshold_r_wilde}, threshold_g: {threshold_g_wilde}, threshold_b: {threshold_b_wilde}')
1.2.3 Labelmasken mittels morphologischer Operationen verbessern¶
Morphologische Operationen sind:
- Dilation (Vergrösserung)
- Erosion (Verkleinerung)
- Opening und Closing (Erosion + Dilation)
Für jede angewendete Operation muss erklärt werden, warum diese angewendet wurde in 1-2 Sätzen.
image_mask_cv = cv.cvtColor(image_mask, cv.COLOR_RGB2GRAY)
image_mask_wilde_cv = cv.cvtColor(image_mask_wilde, cv.COLOR_RGB2GRAY)
1.2.3.1 Dilation (Vergrösserung)¶
Hier testen wir unsere Dilation auf der Segmentierungsmaske der Handy Entons. Dadurch vergrössert sich die Maske. Die Dilation ist für beide Segmentierungsmaske nur bedingt eine geeignete Variante. Bei den Segmentierungs Maske von Handy Entons können wir erkennen, dass diese nicht komplett "sauber" ist. Durch die Dilation ermöglichen wir somit einerseits, dass sich die Segementierungs Maske vergrössert aber auch innen "sauberer" wird.
Bei den Segmentierungs Maske von wilden Entons können wir erkennen, dass die Dilation nicht geeignet ist. Die Maske wird zwar vergrössert, jeoch haben wir schon bei der Segmentierung mittels Thresholding feststellen können, dass Felsen im Hintergrund ebenfalls mit segmentiert wurde. Hier würde es sich eignene zuerst mittels Erosion die Maske zu verkleinern und anschliessend mit Dilation wieder zu vergrössern.
D_dim = 50
D_dim_10 = D_dim // 10
D = np.zeros((D_dim, D_dim), np.uint8)
D[D_dim_10 :-D_dim_10] = 1
D[:, D_dim_10:-D_dim_10] = 1
img_dilation = cv.dilate(image_mask_wilde_cv, D, iterations=1)
plot_three_images(image_mask_wilde_cv, D, img_dilation, titles=['Original Image', 'Dilation Kernel', 'Dilated Image'])
D_dim = 50
D_dim_10 = D_dim // 10
D = np.zeros((D_dim, D_dim), np.uint8)
D[D_dim_10 :-D_dim_10] = 1
D[:, D_dim_10:-D_dim_10] = 1
img_dilation = cv.dilate(image_mask_cv, D, iterations=1)
plot_three_images(image_mask_cv, D, img_dilation, titles=['Original Image', 'Dilation Kernel', 'Dilated Image'])
1.2.3.2 Erosion (Verkleinerung)¶
Bei der Erosion verbessern wir unsere Handy Enton Segmentierungsmaske nur bedingt. Wir erkennen in der Maske, links von der Mitte zwei weisse Pixel die durch die Erosion dann verschwindet, nehmen aber jedoch in Kauf, dass die Enton Masken innen nicht mehr komplett weiss sind. Hier würde es sich eigenen, wenn wir zuerst die Maske mittels Erosion die Pixelwerte die uns nicht interessieren oder zum Objekt gehören entfernen und dann mittels Dilation wieder sauber machen.
Nach mehrfachem austesten bei der Erosion bei den Wilden Entons, konnten wir nur bedingt eine Verbesserung festestellen. Es lassen sich zwar die Felsen im Hintergrund teilweise entfernen, jedoch werden auch die Zielobjekte massiv verkleinert und somit nicht mehr brauchbar gemacht.
E_dim = 10
E_dim_10 = E_dim // 10
E = np.ones((E_dim, E_dim), np.uint8)
E[E_dim_10 :-E_dim_10] = 0
E[:, E_dim_10:-E_dim_10] = 0
img_erosion = cv.erode(image_mask_wilde_cv, E, iterations=3)
plot_three_images(image_mask_wilde_cv, E, img_erosion, titles=['Original Image', 'Erosion Kernel', 'Eroded Image'])
E_dim = 10
E_dim_10 = E_dim // 10
E = np.ones((E_dim, E_dim), np.uint8)
E[E_dim_10 :-E_dim_10] = 0
E[:, E_dim_10:-E_dim_10] = 0
img_erosion = cv.erode(image_mask_cv, E, iterations=1)
plot_three_images(image_mask_cv, E, img_erosion, titles=['Original Image', 'Erosion Kernel', 'Eroded Image'])
1.2.3.4 Opening und Closing (Erosion + Dilation)¶
Hier kombinieren wir unsere Erkentnisse von Oben für beide Segmentierungsmaske.
Für die Handy Enton Segmentierungsmaske fangen wir zuerst mit der Erosion an, um die weisen Pixel ausserhalb unserer Enton Maske zu entfernen. Anschliessend wenden wir die Dilation an, um die Maske wieder zu vergrössern. Somit erhalten wir eine saubere Maske.
Bei den Wilden Entons Segmentierungsmaske wenden wir ebenfalls zuerst die Erosion an und anschliessend die Dilation.
E_dim = 10
E_dim_10 = E_dim // 10
E = np.ones((E_dim, E_dim), np.uint8)
E[E_dim_10 :-E_dim_10] = 0
E[:, E_dim_10:-E_dim_10] = 0
D_dim = 10
D_dim_10 = D_dim // 10
D = np.zeros((D_dim, D_dim), np.uint8)
D[D_dim_10 :-D_dim_10] = 1
D[:, D_dim_10:-D_dim_10] = 1
ero_dil_img = cv.erode(image_mask_wilde_cv, E, iterations=3)
ero_dil_img = cv.dilate(ero_dil_img, D, iterations=1)
extracted_object_ero_dil = extract_object(image_wilde_array, ero_dil_img)
plot_three_images(image_mask_wilde_cv, ero_dil_img, extracted_object_ero_dil, titles=['Segmentation Mask', 'Eroded - Dilation Image', 'Extracted Object'])
E_dim = 10
E_dim_10 = E_dim // 10
E = np.ones((E_dim, E_dim), np.uint8)
E[E_dim_10 :-E_dim_10] = 0
E[:, E_dim_10:-E_dim_10] = 0
D_dim = 10
D_dim_10 = D_dim // 10
D = np.zeros((D_dim, D_dim), np.uint8)
D[D_dim_10 :-D_dim_10] = 1
D[:, D_dim_10:-D_dim_10] = 1
ero_dil_img = cv.erode(image_mask_cv, E, iterations=5)
ero_dil_img = cv.dilate(ero_dil_img, D, iterations=3)
extracted_object_ero_dil = extract_object(image_handy_array, ero_dil_img)
plot_three_images(image_mask_cv, ero_dil_img, extracted_object_ero_dil, titles=['Segmentation Mask', 'Eroded - Dilation Image', 'Extracted Object'])
1.2.4 Extrahieren der einzelnen Objekte¶
Weil die Extraktion der wilen Entons nicht optimal funktioniert hat, haben ich mich dazu entschieden nur noch mit den Handy Entons weiter zu arbeiten. Hier in diesem Abschnitt eine Übersicht, über die extrahierten Objekte durch Segmentierung, Erosion und Dilation.
extracted_object_dil = extract_object(image_handy_array, img_dilation)
extracted_object_ero = extract_object(image_handy_array, img_erosion)
extracted_object_ero_dil = extract_object(image_handy_array, ero_dil_img)
extracted_object_mask = extract_object(image_handy_array, image_mask)
plot_three_images(image_handy_array, extracted_object_ero_dil, extracted_object_mask, titles=['Original Image', 'Extracted Object', 'Segmentation Mask'])
plot_three_images(extracted_object_dil, extracted_object_ero, extracted_object_ero_dil, titles=['Dilated Image', 'Eroded Image', 'Eroded - Dilated Image'])
1.2.5 Zählen und Vermessen von Eigenschaften¶
Die Dilated Maske Segmentiert unsere Objekte schlechter als unsere Threshold Maske. Die Eroded Maske sieht verglichen zu unserer Threshold Maske duetlich besser aus. Der Blaue Rahmen ist deutlich kleiner als bei der Threshold Maske. Beim gleichzeitigen Anwenden von Erosion und Dilation erhalten wir eine Maske, die sehr ähnlich aussieht wie unsere Threshold Maske, jedoch die Haare von Enton nicht mehr vorhanden sind wie bei der Eroded Maske.
Aus diesem Grund Arbeiten wir mit der Erooded Maske weiter.
Anzahl Objekte: Aus unserem Bild ist einfach zu zählen, dass wir Gesamthaft 9 Entons im Bild haben.
plot_three_images(image_handy_array, extracted_object_mask, extracted_object_ero, titles=['Original Image', 'Segmentation Mask', 'Extracted Object'])
1.2.6 Erstellen eines Skeletons und Anzahl Pixel bestimmen¶
1.2.6.1 Skeleton erstellen¶
Skeleton der Objekte. Damit es für uns einfacher ist einen Skeleton zu erstellen, schneiden wir einen Enton aus unserer Eriosions Maske aus und erstellen daraus ein Skeleton.
Das geschnittene Enton sieht bei der Skeletonisierung wie eine Metro Karte aus. Dies könnte daran liegen, dass wir ganz feine schwarze Pixel noch in unserem Bild haben. Diese könnten wir mit einer weiteren Dilation entfernen und dann anschliessend wieder das Skeleton erstellen.
image_array_enton = image_handy_array[550:1000, 850:1400]
img_erosion_enton = img_erosion[550:1000, 850:1400]
extracted_object_enton = extract_object(image_array_enton, img_erosion_enton)
img_skeleton = skeleton(img_erosion_enton)
plot_three_images(image_array_enton, img_erosion_enton, img_skeleton, titles=['Original Image', 'Segmentation Mask', 'Extracted Object'])
1.2.6.2 Skeleton Enton verfeinern¶
Wie wir im vorherigen Kapitel gezeigt haben, können wir mittels der Dilation die Segmentierungsmaske von Enton verbssern und anschliessend skeletonisieren. Das Skeleton sieht nun deutlich besser aus als vorher und nun nicht mehr aus wie eine U-Bahn Karte. Man könnte mit viel Fantasie sagen, dass das Skeleton nun wie ein Enton aussieht.
D_dim = 6
D_dim_10 = D_dim // 10
D = np.ones((D_dim, D_dim), np.uint8)
D[D_dim_10 :-D_dim_10] = 0
D[:, D_dim_10:-D_dim_10] = 0
img_erosion_2 = cv.dilate(img_erosion_enton, D, iterations=3)
img_skeleton_2 = skeleton(img_erosion_2)
plot_three_images(image_array_enton, img_erosion_2, img_skeleton_2, titles=['Original Image', 'Eroded Segmentation Mask', 'Extracted Object'])
1.2.6.3 Anzahl Pixel bestimmen¶
Damit wir die Pixel zählen können, nehmen wir unsere Segmentations Maske die aus 0 und 1 besteht und summieren alle 1er auf und erhalten dann die Anzahl Pixel für unsere Objekte. Für unser Objekt können wir somit den Absoluten Anteil sowie den relativen Anteil bestimmen, die das Objekt in unserem Bild einnimmt. Aus unserer Eroded Segmentationsmaske können wir nun feststellen, dass 25% des Bildes aus Enton eingenommen wird.
# call n_pixel
print("Pixelwerte von Enton mittels verbesserter Erosionsmaske aus 1.2.6.2 berechnet:")
n_pixel_in_mask(img_erosion_2, relativ=True, return_value=False)
print()
print("Pixelwerte von Enton mittels Erosionsmaske 1.2.6.1 berechnet:")
n_pixel_in_mask(img_erosion_enton, relativ=True, return_value=False)
print()
print("Pixelwerte von Eroded Segmentierungsmaske aus 1.2.3.2")
n_pixel_in_mask(img_erosion, relativ=True, return_value=False)
print()
Pixelwerte von Enton mittels verbesserter Erosionsmaske aus 1.2.6.2 berechnet: Total number of pixel: 247500 Relation of pixel in the segmentation mask: 0.37 Number of pixel in the segmentation mask: 92598 Pixelwerte von Enton mittels Erosionsmaske 1.2.6.1 berechnet: Total number of pixel: 247500 Relation of pixel in the segmentation mask: 0.32 Number of pixel in the segmentation mask: 78207 Pixelwerte von Eroded Segmentierungsmaske aus 1.2.3.2 Total number of pixel: 2764800 Relation of pixel in the segmentation mask: 0.25 Number of pixel in the segmentation mask: 694029
2 Feature Deskriptoren in Bildern (LE4)¶
2.1 Keypoint Matching¶
Suche ein paar Bilder mit dem gleichen Sujet/Objekt aus, welche das Objekt von unterschiedlichen Blickwinkeln, aus anderer Perspektive, aus unterschiedlicher Distanz oder rotiert zeigen. Wende dann den dir zugeordneten Keypoint Deskriptor {'BRISK'} an, um mindestens zwei deiner Bilder via Keypoints zu "matchen". Wähle dafür geeignete Parameter und eine geeignete Anzahl Keypoints. Erläuere deine Entscheidungen in 1-2 Sätzen. Zeige deine Resultate visuell und stelle 2-3 Beobachtungen auf. Diskutiere in ca. 150 Wörtern wie robust der dir zugeordnete Algorithms {'BRISK'} ist in Bezug auf Transformationen oder unterschiedlicher Aufnahmeverhältnisse (Licht, ...) und dessen rechnerische Effizienz. Zeige mindestens eine dieser Eigenschaften anhand deiner Beispieldaten. Diskutiere die Resultate und Erkenntnisse in 2-3 Sätzen.
Achtung: dies ist eine offene Aufgabenstellung. Setzt euch selbst einen Rahmen für die Beschränkung oder kommt in die Sprechstunde, sofern euch die Grenzen nicht klar sind. Treffende Datenwahl, auf den Punkt gebrachte Kreativität, massvolle Vielfalt und kritisches Denken sind gesucht.
2.1.1 SIFT¶
Damit ich den SIFT Algorithmus mit dem BRISK Algorithmus quantiativ miteinander für das gleiche Bild vergleichen möchte, habe ich mich zuerst mit dem SIFT Algorithmus auseinandergesetzt.
Die Daten die ich verwende für Keypoint Matching sind selbst geschossene Bilder von meinen 3D Druck Figuren, welche durch unterschiedlichen Merkmalen, wie Brille, Haargummies in Schwarz und Orange sowie ein Freundschaftsband in grün-türkis verziert habe. Die Idee ist, dass wir Figuren haben mit unterschiedlichen Merkmalen und mit unterschiedlichen verzierungen, die dem Algorithmus die Möglichkeit geben, dass Keypoint Matching besser zu erkennen.
sift_image = Image.open('data/image/keypoint_image1.jpg')
sift_image_gray = sift_image.convert('L')
sift_image_array = np.array(sift_image)
sift_image_array_gray = np.array(sift_image_gray)
sift_image_rotated = rotate_image(sift_image, 24122023)
sift_image_rotated_gray = rotate_image(sift_image_gray, 24122023)
sift_image_rotated_array = np.array(sift_image_rotated)
sift_image_rotated_array_gray = np.array(sift_image_rotated_gray)
plot_three_images(sift_image_array, sift_image_rotated_array, sift_image_rotated_gray, titles=['Original Image', 'Rotated Image', ' Rotated Image Grey Scale Image'])
sift_image_, keypoints_, descriptors_ = create_SIFT(sift_image_array, grey_image_array)
plot_three_images(sift_image_array, sift_image_array_gray, sift_image_, titles=['Original Image', 'Gray Image', 'SIFT Image'])
sift_image_rotated_, keypoints_rotated, descriptors_rotated = create_SIFT(sift_image_rotated_array, sift_image_rotated_array_gray)
plot_three_images(sift_image_rotated_array, sift_image_rotated_array_gray, sift_image_rotated_, titles=['Original Image', 'Gray Image', 'SIFT Image'])
bf = cv.BFMatcher(cv.NORM_L2, crossCheck=True)
matches = bf.match(descriptors_, descriptors_rotated)
matches = sorted(matches, key=lambda x: x.distance)
sift_image_matches = cv.drawMatches(sift_image_array,
keypoints_,
sift_image_rotated_array,
keypoints_rotated, matches[:10],
None,
flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS,
matchesThickness=5)
plt.figure(figsize=(12, 6))
plt.imshow(sift_image_matches)
plt.title('Image with Matches')
plt.show()
2.1.2 BRISK¶
Brisk - Binary Robust Invariant Scalable Keypoints
cv.BRISK_create nimmt als Input zwei Bilder die er miteinander verrgleicht sowie weitere Parameter wie, threshold, octaves, patternScale.
Die Dokumentation von cv.Brisk_create ist hier zu finden.
Zu den Parametern wird folgende Beschreibung gegeben:
thresh AGAST detection threshold score.
octaves detection octaves. Use 0 to do single scale.
patternScale apply this scale to the pattern used for sampling the neighbourhood of a keypoint.
Von ChatGPT habe ich folgende Kurzbeschreibung erhalten:
thresh (Schwellenwert):
Dieser Parameter legt einen Schwellenwert für den BRISK-Feature-Detektor fest. Er bestimmt, welche Keypoint-Kandidaten als gültige Keypoints betrachtet werden, basierend auf einem Mass für ihren lokalen Kontrast oder ihre "Eckenstärke".
Keypoints mit einem lokalen Kontrastmass grösser oder gleich thresh werden als gültig angesehen und werden erkannt.
octavess (Oktaven):
BRISK arbeitet auf mehreren Skalen des Eingangsbildes, und octaves gibt die Anzahl der Oktaven oder Skaleniveaus an, die verwendet werden sollen. Jede Oktave repräsentiert eine unterschiedliche Skala des Bildes, von grob bis fein.
Durch Erhöhen der Anzahl der Oktaven kann BRISK Keypoints in verschiedenen Skalen erkennen, was nützlich sein kann, um Objekte in verschiedenen Grössen oder in Bildern mit unterschiedlichen Detailstufen abzustimmen.
patternScale (Musterskala):
BRISK verwendet ein Abtastmuster, um Keypoint-Deskriptoren zu berechnen. patternScale bestimmt die Skala des Musters relativ zur Keypoint-Skala.
Ein kleinerer Wert von patternScale führt zu einem feineren Muster und kann detailliertere Informationen im Keypoint-Deskriptor erfassen. Umgekehrt führt ein grösserer Wert von patternScale zu einem groberen Muster, das mehr globale Informationen erfasst.
Wir nehmen für die Untersuchung von BRISK als Referenzbild immer das erste Bild, welche alle 3D Figuren gut erkennbar von vorne sind. Alle anderen Bilder sind varianten von den gleichen Figuren aus unterschiedlichen Perspektiven, Distanzen. Als ein Feature habe ich dem Dino auf dem Bild eine Brille aufgesetzt um zu schauen, ob der Algorithmus trotz der verzierung der Figur, die Keypoints trotzdem erkennen kann.
Dadurch, dass ich dem Dino eine Brille aufgesetzt habe und im Referenzbild diese nicht vorhanden ist, wurde der Algorithmus beeinflusst. Er erstellt Matching Points von der Brille aus und versucht diese im Referenzbild zu finden, was jedoch nie der Fall sein wird. Wir haben somit den Alorightmus überlistet.
Als ich die Fotos für die Figuren gemacht habe, habe ich diese auf Weisse Papier Blätter gelegt, damit der Boden nicht mit berücksichtigt wird aufgrund von irgendwelchen Muster. Jedoch habe ich die Wand im Hintergrund vergessen, dass der Putz ebenfalls ein Muster hat. Es hat mich zuerst irritiert, dass die Keypoints Matching nicht funktioniert haben, aber bei einer zweiten Betrachtung habe ich gesehen, dass der BRISK Algorithmus viel feiner ist als angenommen. Nähmlich konnte er relativ gut die Wand im Hintergrun erkennen und auch diese gut am richtigen Ort im Referenzbild platzieren.
image_paths = [
'data/image/keypoint_image1.jpg',
'data/image/keypoint_image2.jpg',
'data/image/keypoint_image3.jpg',
'data/image/keypoint_image4.jpg',
'data/image/keypoint_image5.jpg',
'data/image/keypoint_image6.jpg',
'data/image/keypoint_image7.jpg'
]
brisk_images = []
for path in image_paths:
brisk_image = cv.imread(path, cv.IMREAD_GRAYSCALE)
brisk_images.append(brisk_image)
brisk_image = Image.open(image_paths[0])
brisk_image_cv = np.array(brisk_image)
brisk_image_rotated = rotate_image(brisk_image, 24122023)
brisk_image_rotated_cv = np.array(brisk_image_rotated)
result_matches = []
for i, brisk_image in enumerate(brisk_images[1:]):
title = f'Image 1 with Matches from image {i+2}'
result = match_images_with_brisk(brisk_images[0], brisk_image, threshold=10, octaves=3, patternScale=1.0, matches_to_show=10, matchesThickness=5)
result_matches.append((result, title))
for i in range(0, len(result_matches), 3):
batch = result_matches[i:i+3]
images, titles = zip(*batch)
plot_three_images(*images, titles=titles)
2.2 Quantiativer Vergleich von SIFT & BRISK¶
Damit man beide Algorithmen vergleichen kann,habe ich die Anzahl der Linien auf 10 beschrenkt.
Wir erkennen deutlich, dass der SIFT Alorithmus die Keypoints schlechter erkennt als der BRISK Algorithmus. Der SIFT fokkusiert sich zu stark auf meine orange Wand im Hintergrund und erkennt somit die Figuren nicht. Wohingegen der BRISK Algorithmus deutlich besser die Figuren erkennt und auch miteinander matchen kann. Insbesondere erkennen wir auch, dass die besten 10 Keypoints vom BRISK Algorithmus nur von der violetten Figur heraus stammen.
brisk_image_matches = match_images_with_brisk(brisk_image_cv, brisk_image_rotated_cv, threshold=10, octaves=3, patternScale=1.0, matches_to_show=10, matchesThickness=5)
plot_2_images(sift_image_matches, brisk_image_matches, titles=['SIFT Image matches', 'BRISK Image matches'])
3. Peer-Grading¶
Nach Abgabe der Mini-Challenge hast du 1 Woche Zeit drei dir zugeordnete Abgaben von anderen zu bewerten. Die Zuordnung erfolgt via FHNW Peer-Grading-Tool (siehe Link unten). Orientiere dich für die Bewertung an den vorgegebenen Bewertungskriterien (siehe Excel-Datei oder Peer-Grading-Tool). Die Note 5 bedeutet, dass alles erfüllt ist, wie du es von einem guten Data Scientist in der Praxis erwarten würdest. Du startest als Baseline mit der Note 5. Entdeckst du Fehler, geht die Note nach unten. Der Note 5.5 nähert man sich, wenn die Erwartungen übertroffen wurden. Der Note 6 nähert man sich, wenn die Leistung ausserordentlich ist und kritisches Denken, Variabilität, eigene Ideen und Kreativität beinhaltet. Siehe auch Checkliste für Bewertung. Die Benotung soll auf Zehntel gerundet sein. Wer auf Zehntel gerundet mit 0.1 Abweichung die Endnote von der Fachexpertin trifft, kriegt einen Bonus von 0.2 Noten für die eigene Abgabe. In der Sprechstunde vom 16. Januar widmen wir uns dem Peer-Grading. Weiterführende Links: FHNW Peer-Grading-Tool